﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Diagnostics.Tracing;
using Microsoft.Extensions.EnvironmentAbstractions;

namespace Microsoft.DotNet.Cli;

internal sealed class PerformanceLogEventListener : EventListener
{
    internal struct ProviderConfiguration
    {
        internal string Name { get; set; }
        internal EventKeywords Keywords { get; set; }
        internal EventLevel Level { get; set; }
    }

    private static readonly ProviderConfiguration[] s_config =
    [
        new ProviderConfiguration()
        {
            Name = "Microsoft-Dotnet-CLI-Performance",
            Keywords = EventKeywords.All,
            Level = EventLevel.Verbose
        }
    ];

    private const char EventDelimiter = '\n';
    private StreamWriter _writer;

    [ThreadStatic]
    private static StringBuilder s_builder = new();

    internal static PerformanceLogEventListener Create(IFileSystem fileSystem, string logDirectory)
    {
        // Only create a listener if the log directory exists.
        if (string.IsNullOrWhiteSpace(logDirectory) || !fileSystem.Directory.Exists(logDirectory))
        {
            return null;
        }

        PerformanceLogEventListener eventListener = null;
        try
        {
            // Initialization happens as a separate step and not in the constructor to ensure that
            // if an exception is thrown during init, we have the opportunity to dispose of the listener,
            // which will disable any EventSources that have been enabled.  Any EventSources that existed before
            // this EventListener will be passed to OnEventSourceCreated before our constructor is called, so
            // we if we do this work in the constructor, and don't get an opportunity to call Dispose, the
            // EventSources will remain enabled even if there aren't any consuming EventListeners.
            eventListener = new PerformanceLogEventListener();
            eventListener.Initialize(fileSystem, logDirectory);
        }
        catch
        {
            if (eventListener != null)
            {
                eventListener.Dispose();
            }
        }

        return eventListener;
    }

    private PerformanceLogEventListener()
    {
    }

    internal void Initialize(IFileSystem fileSystem, string logDirectory)
    {
        // Use a GUID disambiguator to make sure that we have a unique file name.
        string logFilePath = Path.Combine(logDirectory, $"perf-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")}.log");

        Stream outputStream = fileSystem.File.OpenFile(
            logFilePath,
            FileMode.Create,    // Create or overwrite.
            FileAccess.Write,   // Open for writing.
            FileShare.Read,     // Allow others to read.
            4096,               // Default buffer size.
            FileOptions.None);  // No hints about how the file will be written.

        _writer = new StreamWriter(outputStream);
    }

    public override void Dispose()
    {
        lock (this)
        {
            if (_writer != null)
            {
                _writer.Dispose();
                _writer = null;
            }
        }

        base.Dispose();
    }

    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        try
        {
            // Enable the provider if it matches a requested configuration.
            foreach (ProviderConfiguration entry in s_config)
            {
                if (entry.Name.Equals(eventSource.Name))
                {
                    EnableEvents(eventSource, entry.Level, entry.Keywords);
                }
            }
        }
        catch
        {
            // If we fail to enable, just skip it and continue.
        }

        base.OnEventSourceCreated(eventSource);
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        try
        {
            if (s_builder == null)
            {
                s_builder = new StringBuilder();
            }
            else
            {
                s_builder.Clear();
            }

            s_builder.Append($"[{DateTime.UtcNow.ToString("o")}] Event={eventData.EventSource.Name}/{eventData.EventName} ProcessID={Environment.ProcessId} ThreadID={Thread.CurrentThread.ManagedThreadId}\t ");
            for (int i = 0; i < eventData.PayloadNames.Count; i++)
            {
                s_builder.Append($"{eventData.PayloadNames[i]}=\"{eventData.Payload[i]}\" ");
            }

            lock (this)
            {
                if (_writer != null)
                {
                    foreach (ReadOnlyMemory<char> mem in s_builder.GetChunks())
                    {
                        _writer.Write(mem);
                    }
                    _writer.Write(EventDelimiter);
                }
            }
        }
        catch
        {
            // If we fail to log an event, just skip it and continue.
        }

        base.OnEventWritten(eventData);
    }
}
